Documentation for Users  1.3.1
Perception Toolbox for Virtual Reality (PTVR) Manual
mnread_vr_experiment.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 """
4 IMPORTANT notes:
5 Note 1: Make sure to add in the python Path manager the following path:
6  ...\Python_Scripts\Experiments\MNREAD\MNREAD_VR_Experiment\Algorithms
7  containing the different libraries needed for this main code
8 Note 2: When you open the .csv results file (say with the opensource LibreOffice),
9  make sure to solely use the separator ";" (i.e. the semi-colon character)
10 
11 
12 goal : This experiment recreates the MNRead test in VR
13 
14 how to use : Execute this script to launch the entire experiment.
15 The main parameters of the experiment have to be set in the file mnread_vr_parameters.py
16 
17 predictions : An interface allowing you to start the experiment should be displayed on
18 the PC.
19 For each scene, a new sentence is displayed in the headset. An interface should be
20 visible on the PC screen to interact with the experiment
21 
22 tech note : https://notes.inria.fr/qJt3btPVTnS5zXckWjzn2Q
23 
24 Created on : Tue May 17 11:22:36 2022
25 @author:jdelacha
26 """
27 
28 
29 import PTVR.SystemUtils
30 
31 import numpy as np
32 from PTVR.Visual import The3DWorld
33 from PTVR.Stimuli.Scenes import VisualScene
34 from PTVR.Stimuli.Objects import TangentScreen, Text
36 import Algorithms.mnread_phrase_cutting as mnread
37 import PTVR.Tools as tools
38 import mnread_vr_parameters as parameters
39 import mnread_vr_interface as interface
40 import mnread_vr_interactions as interactions
42 from PTVR.Pointing.PointingCursor import BinocularReticle, ReticleContingency
43 import PTVR.Stimuli.Color as color
44 
45 # =============================================================================
46 # PARAMETERS #
47 # =============================================================================
48 
49 
50 headPOV = parameters.height_of_subject_in_m
51 # 1.2 is the height of the static POV when the user is sitting
52 current_coordinate_system = np.array([0, headPOV, 0])
53 informations_text_size = parameters.visual_angle_of_centered_object
54 
55 # Current tangent screen used in the operator interface to get the distance between the headPOV and the screen
56 current_tangent_screen = []
57 empty_screen = []
58 new_origin_coordinates = []
59 # =============================================================================
60 # END PARAMETERS #
61 # =============================================================================
62 
63 
64 def create_scotoma(scene):
65  if (parameters.scotoma_visible):
66  scotoma = BinocularReticle(
67  contingent_type=ReticleContingency.GAZE) # reticle
68  scotoma.sprite = parameters.scotoma_image
69  scotoma.spriteAngularRadiusX = parameters.scotoma_x_angular_radius
70  scotoma.spriteAngularRadiusY = parameters.scotoma_y_angular_radius
71  scene.AddPointingCursor(scotoma)
72 
73  if (parameters.reticle_visible):
74  reticle_gaze_color = color.RGBColor(r=0, g=0, b=0.9)
75  my_reticle_outer_radius_deg = 6.
76  my_reticle_inner_radius_deg = 2.
77 
78  # The reticle resolution in degrees
79  my_image_resolution_pix = 512
80 
81  # The reticle color and width
82  my_reticle_color = reticle_gaze_color
83  my_reticle_line_width_deg = 0.5
84 
85  reticle_gen = RG.ReticleImageFromDrawing(image_resolution_pix=my_image_resolution_pix,
86  reticle_outer_radius_deg=my_reticle_outer_radius_deg,
87  reticle_inner_radius_deg=my_reticle_inner_radius_deg,
88  reticle_color=my_reticle_color,
89  reticle_line_width_deg=my_reticle_line_width_deg)
90 
91  reticle_eyes = BinocularReticle(
92  contingent_type=ReticleContingency.GAZE)
93  reticle_eyes.SetMaxDistanceProjectionOptions(
94  max_distance=parameters.viewing_distance_in_m, is_projected=False)
95  reticle_gen.Initialize(new_image_resolution_pix=my_image_resolution_pix, new_reticle_inner_radius=my_reticle_inner_radius_deg,
96  newreticle_outer_radius_deg=my_reticle_outer_radius_deg, isDefaultReticle=True, reticle_color=reticle_gaze_color, reticle_line_width_deg=my_reticle_line_width_deg)
97  reticle_eyes.SetCustomReticleGenerator(reticle_gen)
98  scene.AddPointingCursor(reticle_eyes)
99 
100 
101 def create_tangent_screen(visual_angle_of_centered_object, scene, is_active, first_screen=False):
102 
103  screen_width = 18 * tools.visual_angle_to_size_on_perpendicular_plane(visual_angle_of_centered_object_in_deg=visual_angle_of_centered_object,
104  viewing_distance_in_m=parameters.viewing_distance_in_m)
105 
106  screen_height = 8 * tools.visual_angle_to_size_on_perpendicular_plane(visual_angle_of_centered_object_in_deg=visual_angle_of_centered_object,
107  viewing_distance_in_m=parameters.viewing_distance_in_m)
108 
109  if (is_active and parameters.record_eye_gaze and first_screen == False):
110  tangent_screen = TangentScreen(position_in_current_CS=np.array([0, 0, (parameters.viewing_distance_in_m)]),
111  color=parameters.tangent_screen_color,
112  size_in_meters=np.array(
113  [screen_width, screen_height]),
114  is_active=is_active,
115  output_gaze=True)
116  else:
117  tangent_screen = TangentScreen(position_in_current_CS=np.array([0, 0, (parameters.viewing_distance_in_m)]),
118  color=parameters.tangent_screen_color,
119  size_in_meters=np.array(
120  [screen_width, screen_height]),
121  is_active=is_active
122  )
123 
124  scene.fill_in_results_file_column(
125  column_name=parameters.resultfile_tangent_screen_width_in_m, value=screen_width)
126  scene.fill_in_results_file_column(
127  column_name=parameters.resultfile_tangent_screen_height_in_m, value=screen_height)
128  return tangent_screen
129 
130 
131 def create_scene(phrase_to_display,
132  visual_angle_of_centered_object,
133  scene, my_world, phrase_to_write="", tag="",
134  logmar_value=0):
135  """
136  This function is used to create each scene displayed
137  """
138 
139  # The tangent screen is the screen behind the text
140  # The size is set so that the text fits the screen borders
141 
142  tangent_screen = create_tangent_screen(
143  visual_angle_of_centered_object=visual_angle_of_centered_object, scene=scene, is_active=True)
144 
145  x_height_in_meters = tools.visual_angle_to_size_on_perpendicular_plane(visual_angle_of_centered_object_in_deg=visual_angle_of_centered_object,
146  viewing_distance_in_m=parameters.viewing_distance_in_m)
147 
148  # Text displayed on the screen
149  text = Text(text=phrase_to_display,
150  font_name=parameters.font_to_use,
151  color=parameters.text_color,
152  horizontal_alignment="Flush",
153  vertical_alignment="Middle", # "Middle",
154  visual_angle_of_centered_x_height_deg=visual_angle_of_centered_object,
155  )
156 
157  text.isWrapping = True
158  text.textboxScale = np.array([(17.32 * x_height_in_meters), 1.0])
159 
160  # text.SetMnreadFormat(x_height_in_meters=tools.visual_angle_to_size_on_perpendicular_plane(visual_angle_of_centered_object_in_deg=visual_angle_of_centered_object,
161  # viewing_distance_in_m=parameters.viewing_distance_in_m))
162 
163  # Fill columns in the result file
164 
165  scene.fill_in_results_file_column(
166  column_name=parameters.resultfile_sentence_display, value=phrase_to_write)
167  scene.fill_in_results_file_column(
168  column_name=parameters.resultfile_sentence_tag, value=tag)
169  scene.fill_in_results_file_column(
170  column_name=parameters.resultfile_visual_angle, value=str(visual_angle_of_centered_object))
171  scene.fill_in_results_file_column(
172  column_name=parameters.resultfile_misread_words, value="")
173  scene.fill_in_results_file_column(
174  column_name=parameters.resultfile_logmar, value=str(logmar_value))
175  scene.fill_in_results_file_column(
176  column_name=parameters.resultfile_viewing_distance, value=parameters.viewing_distance_in_m)
177  scene.fill_in_results_file_column(
178  column_name=parameters.resultfile_sentence_number, value=interactions.counter_sentences)
179  scene.fill_in_results_file_column(
180  column_name=parameters.resultfile_tangent_screen_id, value=tangent_screen.id)
181 
182  # Empty screen displayed between each sentence, which has a size adapted to the next sentence
183 
184  next_visual_angle = tools.logmar_to_visual_angle_in_degrees(
185  logmar_value - parameters.decreasing_size)
186  if (parameters.sentences_testing_mode):
187  empty_tangent_screen = create_tangent_screen(
188  visual_angle_of_centered_object=visual_angle_of_centered_object, scene=scene, is_active=False)
189  else:
190  empty_tangent_screen = create_tangent_screen(
191  visual_angle_of_centered_object=next_visual_angle, scene=scene, is_active=False)
192 
193  # Placement of the elements in the scene
194  # tangent_screen._place_object_on_tangent_screen(objToPlace = text,
195  # eccentricity=0,
196  # halfMeridian=0)
197  text.set_perimetric_coordinates_on_screen(tangent_screen,
198  eccentricity_local_deg=0.0,
199  half_meridian_local_deg=0.0
200  )
201  scene.place(tangent_screen, my_world)
202  scene.place(empty_tangent_screen, my_world)
203  scene.place(text, my_world)
204 
205  current_tangent_screen.append(tangent_screen)
206  empty_screen.append(empty_tangent_screen)
207  # Add the scene to the experiment
208  my_world.add_scene(scene)
209 
210 
212  """
213  At each scene a new sentence is displayed. Each new phrase has a decreasing size.
214  """
215 
216  # Variable used to store the smallest logmar displayed in order to calculate the reading acuity
217  smallest_logmar = 0
218 
219  # Variable with the current visual angle. Used to decrease the size of each sentence
220  current_visual_angle = parameters.visual_angle_of_centered_object
221 
222  if (parameters.start_logmar):
223  current_visual_angle = tools.logmar_to_visual_angle_in_degrees(
224  parameters.start_logmar)
225 
226  for i in range(parameters.number_of_sentences_to_use):
227  interactions.counter_sentences += 1
228  phrase_to_display = parameters.phrases_to_display[i].rstrip("\n")
229  sentence_tag = ""
230  if (parameters.use_tagged_sentences):
231  sentence_tag = parameters.sentence_tag[i]
232  # conversion degrees to logmar
233  logmar_value = tools.visual_angle_in_degrees_to_logmar(
234  current_visual_angle)
235  # Transformation of the phrase in order to have 3 lines following the MNRead's standards
236  list_cutted_phrases_from_mnread_phrase_cutting = mnread.mnread_phrase_cutting(
237  phrase_to_display)
238 
239  phrase_with_back_to_line = str(list_cutted_phrases_from_mnread_phrase_cutting[0]) + "\n" + str(
240  list_cutted_phrases_from_mnread_phrase_cutting[1]) + "\n" + str(list_cutted_phrases_from_mnread_phrase_cutting[2])
241 
242  # Creation of the scene with the displayed phrase
243  t1 = VisualScene(background_color=parameters.scene_background_color)
244  create_scotoma(t1)
245  create_scene(phrase_to_display=phrase_with_back_to_line,
246  visual_angle_of_centered_object=current_visual_angle,
247  scene=t1,
248  my_world=my_world,
249  phrase_to_write=phrase_to_display,
250  tag=sentence_tag,
251  logmar_value=logmar_value)
252 
253  # Creation of the main UI template
254  interface.create_background_image_UI(t1)
255  # Creation of one button per word
256  interactions.create_words_UI(
257  t1, my_world, list_cutted_phrases_from_mnread_phrase_cutting)
258  # Creation of main button UI to interact with the experiment
259  interactions.create_main_UI(t1, my_world, list_cutted_phrases_from_mnread_phrase_cutting,
260  current_tangent_screen[0], empty_screen[0], logmar_value, current_visual_angle)
261  current_tangent_screen.clear()
262  empty_screen.clear()
263 
264  if (i == len(parameters.phrases_to_display)):
265  smallest_logmar = logmar_value
266 
267  # Management of the current visual angle in order to decrease the size of the next phrase
268  if (parameters.sentences_testing_mode == False):
269  current_visual_angle = tools.logmar_to_visual_angle_in_degrees(
270  logmar_value - parameters.decreasing_size)
271 
272  # Creation of the end scene
273  end_scene = VisualScene()
274  create_scotoma(end_scene)
275  interface.create_background_image_UI(end_scene)
276  interactions.end_experiment(my_world, end_scene, smallest_logmar)
277  my_world.add_scene(end_scene)
278 
279 
280 def main():
281 
282  # Set-up the experiment file
283  my_world = The3DWorld(
284  operatorMode=True, name_of_subject=parameters.subject_name)
285  my_world.translate_coordinate_system_along_global(
286  current_coordinate_system)
287  start_scene = VisualScene(
288  background_color=parameters.scene_background_color)
289  my_world.add_scene(start_scene)
290 
291  current_visual_angle = parameters.visual_angle_of_centered_object
292  if (parameters.start_logmar):
293  current_visual_angle = tools.logmar_to_visual_angle_in_degrees(
294  parameters.start_logmar)
295 
296  first_empty_screen = create_tangent_screen(
297  current_visual_angle, start_scene, is_active=True, first_screen=True)
298  start_scene.place(first_empty_screen, my_world)
299 
300  create_scotoma(start_scene)
301 
302  interactions.start_experiment(start_scene, my_world, first_empty_screen)
303 
304  # Write The3DWorld to file
305  for i in range(1, parameters.number_of_sentences_to_use):
306  my_world.scenes[my_world.id_scene[i]].interaction[-1].callbacks[0].SetNextScene(
307  id_next_scene=my_world.id_scene[-1])
308  my_world.write()
309  print("The experiment has been written.")
310 
311 
312 if __name__ == "__main__":
313  main()
314 
315  PTVR.SystemUtils.LaunchThe3DWorld(parameters.subject_name)
def LaunchThe3DWorld(jsonFileCategory="Externals")
Definition: SystemUtils.py:182
def create_scene(phrase_to_display, visual_angle_of_centered_object, scene, my_world, phrase_to_write="", tag="", logmar_value=0)
def create_tangent_screen(visual_angle_of_centered_object, scene, is_active, first_screen=False)
def sentences_sequence_mnread(my_world)